// Copyright (C) 2023 即时通讯网(52im.net) & Jack Jiang.
// The RainbowChat Project. All rights reserved.
// 
// 【本产品为著作权产品，合法授权后请放心使用，禁止外传！】
// 【本次授权给：<北京小羊驼科技有限公司>，授权编号：<NT231212144350>，代码指纹：<A.702363430.550>，技术对接人微信：<ID: wxid_wbyootvkdcgj22>】
// 【授权寄送：<收件：苏先生、地址：北京市大兴区北京密码西区6号楼1单元301、电话：18613885610、邮箱：bd@huiyuanxiang-inc.com>】
// 
// 【本系列产品在国家版权局的著作权登记信息如下】：
// 1）国家版权局登记名(简称)和权证号：RainbowChat    （证书号：软著登字第1220494号、登记号：2016SR041877）
// 2）国家版权局登记名(简称)和权证号：RainbowChat-Web（证书号：软著登字第3743440号、登记号：2019SR0322683）
// 3）国家版权局登记名(简称)和权证号：RainbowAV      （证书号：软著登字第2262004号、登记号：2017SR676720）
// 4）国家版权局登记名(简称)和权证号：MobileIMSDK-Web（证书号：软著登字第2262073号、登记号：2017SR676789）
// 5）国家版权局登记名(简称)和权证号：MobileIMSDK    （证书号：软著登字第1220581号、登记号：2016SR041964）
// * 著作权所有人：江顺/苏州网际时代信息科技有限公司
// 
// 【违法或违规使用投诉和举报方式】：
// 联系邮件：jack.jiang@52im.net
// 联系微信：hellojackjiang
// 联系QQ号：413980957
// 授权说明：http://www.52im.net/thread-1115-1-1.html
// 官方社区：http://www.52im.net
package com.x52im.rainbowchat.http.file.d;

import java.io.File;
import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import ws.schild.jave.AudioAttributes;
import ws.schild.jave.Encoder;
import ws.schild.jave.EncodingAttributes;
import ws.schild.jave.MultimediaObject;

import com.eva.epc.common.file.FileHelper;
import com.eva.epc.common.util.CommonUtils;
import com.eva.framework.httpfile.FileDownload;
import com.eva.framework.utils.LoggerFactory;
import com.x52im.rainbowchat.BaseConf;
import com.x52im.rainbowchat.http.logic.LogicUtils;
import com.x52im.rainbowchat.im.ChatServerLauncher;

/**
 * 一个图片、语音留言、个人介绍语音、相册照片等的通用下载实用类。
 * <p>
 * <b>补充：</b>下传文件时，Android、iOS、Web等都是可以在通过上传URL中将
 * 参数带过来，所以直接在HttpServletRequest里拿出url里的参数就行了。
 * 
 * @since RainbowChat 2.2
 */
//FIXME: 在更苛刻的安全性要求下，可考虑要求客户端把token字段带上来，以便进行更强的合法身份检查
public class BinaryDownloader extends javax.servlet.http.HttpServlet 
	implements javax.servlet.Servlet
{
	/** 一个1像素黑色图片 */
	public final static String ONE_PIXEL_BLACK_FILE_NAME = "on_pixel_black.png";
	
	/* (non-Java-doc)
	 * @see javax.servlet.http.HttpServlet#HttpServlet()
	 */
	public BinaryDownloader() 
	{
		super();
	} 
	
	/**
	 * Servlet规范中本protected方法由public void service(,,)方法调用, 而public void service(,,)
	 * 则由Web容器调用(所以它是public公共方法),因此一般情况下尽量避免重写public void service(,,)方法
	 * ,而重写父类的本protedted方法则比较合理.
	 */
	protected void service(HttpServletRequest req,HttpServletResponse res)
		throws ServletException,IOException
	{
		LoggerFactory.getLog().debug("[HTTP][BINARY_DOWN]下载2进制：你传过来的参数" +
				"action="+req.getParameter("action")
				+",user_uid="+req.getParameter("user_uid") // 读取图片的人（目前服务端保留，暂未使用）
				+",file_name="+req.getParameter("file_name")
				+",need_dump="+req.getParameter("need_dump")
				+",[---- "+req.getContextPath()+"-"+req.getRequestURL()+"]");
		
		String action = req.getParameter("action");
		String userUid = req.getParameter("user_uid");
		String file_name = req.getParameter("file_name");
		
		//【是否需要转储】：true表示需要转储，否则不需要. 转储是用于图片消息接收方在打开了该图片消息完整图后
		// * 通知服务端将此图进行转储（转储的可能性有2种：直接删除掉、移到其它存储位置），转储的目的是防止大量用户的大量图片
		// * 被读过后还存储在服务器上，加大了服务器的存储压力。<b>注意：</b><u>读取缩略图时无需转储！</u>
		String need_dump = req.getParameter("need_dump");
		
		// 注意：跟web互通模式开启的情况下，dump参数将强制失败，原因为web端不像app端有缓存能力，如果像app端读后
		//       就dump，则web端下次加载聊天记录时，这个图片或语音等文件就会因被转储而不存在，影响用户体验。-20190330 by Jack Jiang
		boolean needDump = (ChatServerLauncher.bridgeEnabled?false:(need_dump != null? "1".equals(need_dump):false));
		
		try
		{
			// 图片聊天消息里的图片文件下载
			if("image_d".equals(action))
			{
				// 处理图片下载
				processBinaryDownload(res,  userUid
						, BaseConf.getInstance().getDIR_USER_IMAGE_UPLOAD_UNREAD()
						, file_name, FileDownload.CONTENT_TYPE_JPEG);
				// 需要转储
				if(needDump){
					String imageSrcPath = BaseConf.getInstance().getDIR_USER_IMAGE_UPLOAD_UNREAD()+file_name;
					String imageThunmbSrcPath = BaseConf.getInstance().getDIR_USER_IMAGE_UPLOAD_UNREAD()+"th_"+file_name;
					
					// creates the directory if it does not exist
					File dumpDir = new File(BaseConf.getInstance().getDIR_USER_IMAGE_UPLOAD_READ());
					if (!dumpDir.exists())
						dumpDir.mkdirs();
					
					// 先把图片主文件copy到已读目录（TODO 2014-02-08起为了分析用户行为而转储，以后或可直接删除，因为用户的图片数据意义不大！）
					boolean copySuccess = FileHelper.copyFile(imageSrcPath
							, BaseConf.getInstance().getDIR_USER_IMAGE_UPLOAD_READ()+file_name);
					// 再把缩略图copy到已读目录（TODO 2014-02-08起为了分析用户行为而转储，以后或可直接删除，因为用户的图片数据意义不大！）
					boolean copySuccess_th = FileHelper.copyFile(imageThunmbSrcPath
							, BaseConf.getInstance().getDIR_USER_IMAGE_UPLOAD_READ()+"th_"+file_name);
					// 最后将图片从原目录中删除
					if(copySuccess && copySuccess_th){
						FileHelper.deleteFile(imageSrcPath);
						FileHelper.deleteFile(imageThunmbSrcPath);
					}
				}
			}
			// 语音留言消息里的语音文件下载（AMR原文件下载）：当前主要用于APP端
			else if("voice_d".equals(action))
			{
				// 处理语音留言下载
				processBinaryDownload(res,  userUid
						, BaseConf.getInstance().getDIR_USER_VOICE_UPLOAD_UNREAD(), file_name);
				// 需要转储
				if(needDump){
					String voiceSrcPath = BaseConf.getInstance().getDIR_USER_VOICE_UPLOAD_UNREAD()+file_name;

					// creates the directory if it does not exist
					File dumpDir = new File(BaseConf.getInstance().getDIR_USER_VOICE_UPLOAD_READ());
					if (!dumpDir.exists())
						dumpDir.mkdirs();

					// 先把语音留言主文件copy到已读目录（TODO 2014-02-08起为了分析用户行为而转储，以后或可直接删除，因为用户的图片数据意义不大！）
					boolean copySuccess = FileHelper.copyFile(voiceSrcPath
							, BaseConf.getInstance().getDIR_USER_VOICE_UPLOAD_READ()+file_name);
					// 最后将语音留言从原目录中删除
					if(copySuccess)
						FileHelper.deleteFile(voiceSrcPath);
				}
			}
			// @since 4.5 - 20181206 by Jack Jiang
			// 语音留言消息里的语音文件下载（AMR转码MP3格式后的文件下载）：当前主要用于Web端
			// （因Web端html5不能支持app中广泛使用的amr音频格式，所以为了在web端能播放语音留言，只能先转码MP3后再供下载播放）
			else if("voice_mp3_d".equals(action))
			{
				String originalAMRFilePath = BaseConf.getInstance().getDIR_USER_VOICE_UPLOAD_UNREAD()+file_name;
				
				String destMP3FileName = file_name+".mp3";
				String destMP3FilePath = BaseConf.getInstance().getDIR_USER_VOICE_UPLOAD_TO_MP3()+destMP3FileName;
				
				boolean isMP3Exists = FileHelper.isFileExist(destMP3FilePath);
				boolean isAMRExists = FileHelper.isFileExist(originalAMRFilePath);
				
				boolean convertFial = false;
				
				// mp3文件是否已经存在
				if(isMP3Exists){
					LoggerFactory.getLog().debug("[HTTP][BINARY_DOWN-mp3] 语音消息文件"+file_name+"的MP3转码文件已存在，无需转码，直接为用户下载之...");
					
					// 处理语音留言mp3格式的下载
					processBinaryDownload(res,  userUid
							, BaseConf.getInstance().getDIR_USER_VOICE_UPLOAD_TO_MP3()
							, destMP3FileName
							, FileDownload.CONTENT_TYPE_MP3);
				}
				else{
					LoggerFactory.getLog().debug("[HTTP][BINARY_DOWN-mp3] 语音消息文件"+file_name
							+"的MP3转码文件不存在，马上开始转码先..... (期望的mp3路径为："+destMP3FilePath+")");
					
					// amr文件存在的情况下才需要转mp3，不然给空气转码？
					if(isAMRExists){
						// 开始转码
						convertFial = !convertAMRtoMP3(originalAMRFilePath, destMP3FilePath);
						
						// 转码成功
						if(!convertFial){
							// 处理语音留言mp3格式的下载
							processBinaryDownload(res,  userUid
									, BaseConf.getInstance().getDIR_USER_VOICE_UPLOAD_TO_MP3()
									, destMP3FileName
									, FileDownload.CONTENT_TYPE_MP3);
						}
					}
					else{
						LoggerFactory.getLog().warn("[HTTP][BINARY_DOWN-mp3] 将要转码mp3的语音消息文件"+originalAMRFilePath
								+"不存在，无法完成其对应的MP3文件转码，本次下载失败！！");
					}
				}
				
				// 需要转储
				if(needDump && isAMRExists && !convertFial){
					// creates the directory if it does not exist
					File dumpDir = new File(BaseConf.getInstance().getDIR_USER_VOICE_UPLOAD_READ());
					if (!dumpDir.exists())
						dumpDir.mkdirs();

					// 先把语音留言主文件copy到已读目录（TODO 2014-02-08起为了分析用户行为而转储，以后或可直接删除，因为用户的图片数据意义不大！）
					boolean copySuccess = FileHelper.copyFile(originalAMRFilePath
							, BaseConf.getInstance().getDIR_USER_VOICE_UPLOAD_READ()+file_name);
					// 最后将语音留言从原目录中删除
					if(copySuccess)
						FileHelper.deleteFile(originalAMRFilePath);
				}
			}
			// 个人照片下载
			else if("photo_d".equals(action))
			{
				// 此参数为“1”时，表示当照片图片不存在时，会默认返回一个1像素黑色图片（这主要用于改善Web网页里显示用户
				// 相册时的体验，否则当用户没有设置头像时，因为没有图片数据返回而会在页面上显示一个“破图”img元素，那就难看死了！）
				String _onePixelBlackIfNo = req.getParameter("one_pixel_black_if_no");
				boolean onePixelBlackIfNo = (_onePixelBlackIfNo != null && _onePixelBlackIfNo.equals("1"));
				
				File f = new File(BaseConf.getInstance().getDIR_USER_PHOTO_UPLOAD()+file_name);
				// 为web端的“破图”下载1像素黑色图片，改善视觉体验
				if(!f.exists() && onePixelBlackIfNo){
					File onePixelTransparentAvatarImg = new File(BinaryDownloader.class.getResource(ONE_PIXEL_BLACK_FILE_NAME).toURI());
					FileDownload.download(res, onePixelTransparentAvatarImg, ONE_PIXEL_BLACK_FILE_NAME, FileDownload.CONTENT_TYPE_JPEG, false);
				}
				else{
					// 正常处理图片下载
					processBinaryDownload(res,  userUid
							, BaseConf.getInstance().getDIR_USER_PHOTO_UPLOAD()
							, file_name, FileDownload.CONTENT_TYPE_JPEG);
				}
				
				// 如果是综略图的话就不需要尝试更新查看次数了（而且也更新不了，因为db里存放的文件名是全图而非缩略图）
				if(!file_name.startsWith("th_")) {
					// 查看次数+1
					LogicUtils.increaseBinaryViewCount(file_name);
				}
			}
			// 自我介绍里的语音留言文件下载
			else if("pvoice_d".equals(action))
			{
				// 处理语音留言下载
				processBinaryDownload(res,  userUid, BaseConf.getInstance().getDIR_USER_PVOICE_UPLOAD(), file_name);
				// 查看次数+1
				LogicUtils.increaseBinaryViewCount(file_name);
			}
			// @since 4.5 - 20181226 by Jack Jiang
			// 自我介绍里的语音留言文件下载（AMR转码MP3格式后的文件下载）：当前主要用于Web端
			// （因Web端html5不能支持app中广泛使用的amr音频格式，所以为了在web端能播放语音留言，只能先转码MP3后再供下载播放）
			else if("pvoice_mp3_d".equals(action))
			{
	//				// 处理语音留言下载
	//				processBinaryDownload(res,  userUid
	//						, BaseConf.getInstance().getDIR_USER_PVOICE_UPLOAD(), file_name);
	//				// 查看次数+1
	//				LogicUtils.increaseBinaryViewCount(file_name);
					
				String originalAMRFilePath = BaseConf.getInstance().getDIR_USER_PVOICE_UPLOAD()+file_name;
				
				String destMP3FileName = file_name+".mp3";
				String destMP3FilePath = BaseConf.getInstance().getDIR_USER_PVOICE_UPLOAD()+destMP3FileName;
				
				boolean isMP3Exists = FileHelper.isFileExist(destMP3FilePath);
				boolean isAMRExists = FileHelper.isFileExist(originalAMRFilePath);
				
				boolean convertFial = false;
				
				// mp3文件是否已经存在
				if(isMP3Exists)
				{
					LoggerFactory.getLog().debug("[HTTP][BINARY_DOWN-mp3] 个人语音介绍文件"+file_name+"的MP3转码文件已存在，无需转码，直接为用户下载之...");
					
					// 处理语音留言mp3格式的下载
					processBinaryDownload(res,  userUid
							, BaseConf.getInstance().getDIR_USER_PVOICE_UPLOAD(), destMP3FileName);
					// 查看次数+1
					LogicUtils.increaseBinaryViewCount(file_name);
				}
				else
				{
					LoggerFactory.getLog().debug("[HTTP][BINARY_DOWN-mp3] 个人语音介绍文件"+file_name
							+"的MP3转码文件不存在，马上开始转码先..... (期望的mp3路径为："+destMP3FilePath+")");
					
					// amr文件存在的情况下才需要转mp3，不然给空气转码？
					if(isAMRExists){
						// 开始转码
						convertFial = !convertAMRtoMP3(originalAMRFilePath, destMP3FilePath);
						
						// 转码成功
						if(!convertFial){
							// 处理语音留言mp3格式的下载
							processBinaryDownload(res,  userUid
									, BaseConf.getInstance().getDIR_USER_PVOICE_UPLOAD(), destMP3FileName);
							// 查看次数+1
							LogicUtils.increaseBinaryViewCount(file_name);
						}
					}
					else{
						LoggerFactory.getLog().warn("[HTTP][BINARY_DOWN] 将要转码mp3的个人语音介绍文件"+originalAMRFilePath
								+"不存在，无法完成其对应的MP3文件转码，本次下载失败！！");
					}
				}
			}
			// 群头像的图片文件下载
			else if("gavartar_d".equals(action))
			{
				// 此参数为“1”时，表示当群组头像不存在时，会默认返回一个1像素透明图片（这主要用于改善Web网页里显示群组
				// 头像时的体验，否则当群组头像图片因种种原因没有读取成功时，没有图片数据返回而会在页面上显示一个“破图”img元素，那就难看死了！）
				String _onePixelTransparentIfNo = req.getParameter("one_pixel_transparent_if_no");
				boolean onePixelTransparentIfNo = (_onePixelTransparentIfNo != null && _onePixelTransparentIfNo.equals("1"));
				
				File f = new File(BaseConf.getInstance().getDIR_USER_AVATARGROUP_DIR()+file_name);
				// 为web端的“破图”下载1像素透明图片，改善视觉体验
				if(!f.exists() && onePixelTransparentIfNo){
					File onePixelTransparentAvatarImg = new File(BinaryDownloader.class.getResource(UserAvatarDownloader.ONE_PIXEL_TRANSPARENT_AVATAR_FILE_NAME).toURI());
					FileDownload.download(res
							, onePixelTransparentAvatarImg
							, UserAvatarDownloader.ONE_PIXEL_TRANSPARENT_AVATAR_FILE_NAME
							, FileDownload.CONTENT_TYPE_JPEG
							, false);
				}
				else{
					// 处理文件下载
					processBinaryDownload(res,  userUid, BaseConf.getInstance().getDIR_USER_AVATARGROUP_DIR(), file_name);
				}
			}
			// 位置聊天消息里的地图预览图片文件下载
			else if("location_d".equals(action))
			{
				// 处理图片下载
				processBinaryDownload(res,  userUid
						, BaseConf.getInstance().getDIR_USER_LOCATION_UPLOAD_UNREAD()
						, file_name, FileDownload.CONTENT_TYPE_JPEG);
				// 需要转储
				if(needDump){
					String imageSrcPath = BaseConf.getInstance().getDIR_USER_LOCATION_UPLOAD_UNREAD()+file_name;

					// creates the directory if it does not exist
					File dumpDir = new File(BaseConf.getInstance().getDIR_USER_LOCATION_UPLOAD_READ());
					if (!dumpDir.exists())
						dumpDir.mkdirs();

					// 先把图片主文件copy到已读目录（TODO 转储的目的权是为了分析用户行为而转储，不需要的话可直接删除，因为用户的图片数据意义不大！）
					boolean copySuccess = FileHelper.copyFile(imageSrcPath
							, BaseConf.getInstance().getDIR_USER_LOCATION_UPLOAD_READ()+file_name);
				
					// 最后将图片从原目录中删除
					if(copySuccess){
						FileHelper.deleteFile(imageSrcPath);
					}
				}
			}
			// 好友备注中的图片下载
			// TODO: 此下载接口已写好，但因开发计划搁置，客户端暂未实现好友备注中设置图片的功能
			else if("remark_photo_d".equals(action))
			{
				// 此参数为“1”时，表示当照片图片不存在时，会默认返回一个1像素黑色图片（这主要用于改善Web网页里显示用户
				// 相册时的体验，否则当用户没有设置头像时，因为没有图片数据返回而会在页面上显示一个“破图”img元素，那就难看死了！）
				String _onePixelBlackIfNo = req.getParameter("one_pixel_black_if_no");
				boolean onePixelBlackIfNo = (_onePixelBlackIfNo != null && _onePixelBlackIfNo.equals("1"));
				
				File f = new File(BaseConf.getInstance().getDIR_USER_REMARK_PHOTO_UPLOAD()+file_name);
				// 为web端的“破图”下载1像素黑色图片，改善视觉体验
				if(!f.exists() && onePixelBlackIfNo){
					File onePixelTransparentImg = new File(BinaryDownloader.class.getResource(ONE_PIXEL_BLACK_FILE_NAME).toURI());
					FileDownload.download(res, onePixelTransparentImg, ONE_PIXEL_BLACK_FILE_NAME, FileDownload.CONTENT_TYPE_JPEG, false);
				}
				else{
					// 正常处理图片下载
					processBinaryDownload(res,  userUid, BaseConf.getInstance().getDIR_USER_REMARK_PHOTO_UPLOAD(), file_name, FileDownload.CONTENT_TYPE_JPEG);
				}
			}
			else
			{
				LoggerFactory.getLog().warn("[HTTP]无效的action="+action);
			}
		}
		catch (IOException e){
			throw e;
		}
		catch (Exception e2){
			throw new IOException(e2.getMessage(), e2.getCause());
		}
	}
	
	/**
	 * HTTP文件下载实现方法.
	 * <p>
	 * 本方法没有任何异常地返回即意味着成功处理完成.
	 * 
	 * @param res
	 * @param userUid
	 * @param file_dir 要下载的文件所位于的服务器磁盘目录（此dir的结尾需要"/"哦）
	 * @param file_name 要下载的磁盘文件名
	 * @throws IOException
	 */
	private void processBinaryDownload(HttpServletResponse res
			, String userUid, String file_dir, String file_name) throws IOException
	{
		this.processBinaryDownload(res, userUid, file_dir, file_name, FileDownload.CONTENT_TYPE_DEFAULT);
	}
	
	/**
	 * HTTP文件下载实现方法.
	 * <p>
	 * 本方法没有任何异常地返回即意味着成功处理完成.
	 * 
	 * @param res
	 * @param userUid
	 * @param file_dir 要下载的文件所位于的服务器磁盘目录（此dir的结尾需要"/"哦）
	 * @param file_name 要下载的磁盘文件名
	 * @throws IOException
	 */
	private void processBinaryDownload(HttpServletResponse res
			, String userUid, String file_dir, String file_name
			, String contentType) throws IOException
	{
		try
		{
			if(!CommonUtils.isStringEmpty(file_name, true))
			{
				LoggerFactory.getLog().debug("[HTTP]用户【"+userUid+"】要下载的2进制文件 "+file_name
						+"将立即开始处理过程 ......");
				// 实施文件下载
				FileDownload.download(res, file_dir + file_name, file_name, contentType
						, FileDownload.CONTENT_TYPE_JPEG.equals(contentType)?false:true);
			}
			else
				LoggerFactory.getLog().warn("[HTTP]对不起,用户【"+userUid+"】要下载的2进制(file_name="+file_name+")不存在！");
		}
		catch (Exception e)
		{
			LoggerFactory.getLog().warn("[HTTP]用户【"+userUid+"】的下载的2进制文件不存在，本次下次已提前结束("+e.getMessage()+")");
		} 
	}
	
	/**
	 * 将AMR音频格式转码为MP3格式的实用方法。
	 * <p>
	 * 转码库使用的是ffmpeg。
	 * 
	 * @param srcAMRPath amr源文件绝对路径
	 * @param destMP3Path 转码后要保存的mp3文件绝对路径
	 * @return true表示转码和保存等工作完全完成，否则表示失败
	 */
	private static boolean convertAMRtoMP3(String srcAMRPath, String destMP3Path)
	{
		boolean sucess = false;
		
		long t = System.currentTimeMillis();
		try
		{
			File source = new File(srcAMRPath);  
	        File target = new File(destMP3Path);  
	        
	        AudioAttributes audio = new AudioAttributes();  
	        audio.setCodec("libmp3lame");
//	        audio.setBitRate(new Integer(128000));
//	        audio.setChannels(new Integer(2));
//	        audio.setSamplingRate(new Integer(44100));
	        
	        EncodingAttributes attrs = new EncodingAttributes();  
	        attrs.setFormat("mp3");  
	        attrs.setAudioAttributes(audio);  
	        
	        Encoder encoder = new Encoder(); 
	        encoder.encode(new MultimediaObject(source), target, attrs); 
	        
	        sucess = true;
		}
		catch (Exception e)
		{
			LoggerFactory.getLog().warn("[AMRtoMP3] 转码失败【NO】，原因："+e.getMessage()
					+" [srcAMRPath="+srcAMRPath+", destMP3Path="+destMP3Path+"]", e);
		}
		if(sucess)
		{
			LoggerFactory.getLog().info("[AMRtoMP3] 转码完成【OK】， 转码耗时："+(System.currentTimeMillis()-t)
					+" 毫秒  [srcAMRPath="+srcAMRPath+", destMP3Path="+destMP3Path+"]。");
		}
		
		return sucess;
	}
}
